/*******************************************************************************
 * Copyright (c) 2011 Olexandr V. Shaposhnikov.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Olexandr V. Shaposhnikov - initial API and implementation and/or initial documentation
 *******************************************************************************/
package com.funkyroach.use.core.selection;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Stack;

import org.eclipse.dltk.ast.ASTNode;
import org.eclipse.dltk.ast.ASTVisitor;
import org.eclipse.dltk.ast.declarations.Declaration;
import org.eclipse.dltk.ast.declarations.ModuleDeclaration;
import org.eclipse.dltk.ast.expressions.Expression;
import org.eclipse.dltk.codeassist.ScriptSelectionEngine;
import org.eclipse.dltk.compiler.env.IModuleSource;
import org.eclipse.dltk.core.IField;
import org.eclipse.dltk.core.IMethod;
import org.eclipse.dltk.core.IModelElement;
import org.eclipse.dltk.core.IParameter;
import org.eclipse.dltk.core.ISourceModule;
import org.eclipse.dltk.core.IType;
import org.eclipse.dltk.core.ModelException;
import org.eclipse.dltk.core.model.LocalVariable;
import org.eclipse.dltk.core.search.TypeNameMatch;
import org.eclipse.dltk.core.search.TypeNameMatchRequestor;
import org.eclipse.dltk.internal.core.SourceField;
import org.eclipse.dltk.internal.core.SourceMethod;
import org.eclipse.dltk.internal.core.SourceType;

import com.funkyroach.use.core.ast.USArgument;
import com.funkyroach.use.core.ast.USClassDeclaration;
import com.funkyroach.use.core.ast.USFuncDeclaration;
import com.funkyroach.use.core.ast.USModuleDeclaration;
import com.funkyroach.use.core.ast.USPointedExpression;
import com.funkyroach.use.core.ast.USTypeReference;
import com.funkyroach.use.core.ast.USVarReference;
import com.funkyroach.use.core.ast.USVariableDeclaration;
import com.funkyroach.use.core.ast.expr.USMethodCallExpression;
import com.funkyroach.use.core.codeassist.USAssistParser;
import com.funkyroach.use.core.mixin.USMixinModel;
import com.funkyroach.use.core.model.USModelHelper;

public class USSelectionEngine extends ScriptSelectionEngine {
    private ISourceModule sourceModule;
    private USAssistParser parser = new USAssistParser();
    private Set<IModelElement> selection = new HashSet<IModelElement>();
    private SourceMethod sourceMethod;
    private IType sourceClass;
    private USMixinModel mixinModel;

    @Override
    public IModelElement[] select(IModuleSource moduleSource, int offsetStart, int offsetEnd) {
        sourceModule = (ISourceModule) moduleSource.getModelElement();
        mixinModel = USMixinModel.instance(sourceModule.getScriptProject());

        USModuleDeclaration moduleDeclaration = (USModuleDeclaration) this.parser.parse(moduleSource);

        USClassDeclaration classDeclaration = moduleDeclaration.getClassDeclaration();

        sourceClass = sourceModule.getType(classDeclaration.getName());

        ASTNode node = findMinimalNode(moduleDeclaration, offsetStart, offsetEnd + 1);

        if (node instanceof USTypeReference) {
            selectOnTypeReference((USTypeReference) node);
        } else if (node instanceof USVarReference) {
            selectionOnVariable(moduleDeclaration, (USVarReference) node, offsetStart);
        } else if (node instanceof USMethodCallExpression) {
            USMethodCallExpression methodCall = (USMethodCallExpression) node;

            USPointedExpression pointedBy = methodCall.getPointedExpression();
            if (pointedBy != null) {
                USTypeReference type = getTypeReference(pointedBy);
            }
            selection.addAll(selectOnMethdCall(methodCall));
        }

        return (IModelElement[]) selection.toArray(new IModelElement[selection.size()]);
    }

    private USTypeReference getTypeReference(USPointedExpression pointedBy) {
        Expression primExpr = pointedBy.getPrimaryExpression();
        if (primExpr instanceof USVarReference) {

        }
        // TODO Auto-generated method stub
        return null;
    }

    private void selectionOnVariable(USModuleDeclaration moduleDeclaration, USVarReference varRef, final int offsetStart) {
        ASTNode[] fWayToNode = USSelectionEngine.collectWayToNode(moduleDeclaration, varRef);

        String variableName = varRef.getName();

        if (fWayToNode != null) {
            for (int i = 0; i < fWayToNode.length; i++) {
                if (fWayToNode[i] instanceof USFuncDeclaration) {

                    USFuncDeclaration usFuncDecl = (USFuncDeclaration) fWayToNode[i];
                    sourceMethod = (SourceMethod) sourceClass.getMethod(usFuncDecl.getName());
                    if (sourceMethod != null) {
                        try {
                            IParameter[] params = sourceMethod.getParameters();
                            for (int j = 0; j < params.length; j++) {
                                if (params[j].getName().equalsIgnoreCase(variableName)) {
                                    USArgument argument = (USArgument) usFuncDecl.getArgument(variableName);

                                    LocalVariable localVar = new LocalVariable(sourceMethod, variableName,
                                            argument.getNameStart(), argument.getNameEnd(), argument.sourceStart(),
                                            argument.sourceEnd(), argument.getType().getName());
                                    selection.add(localVar);
                                    return;
                                }
                            }
                        } catch (ModelException e) {
                        }

                        try {
                            IModelElement[] children = sourceMethod.getChildren();
                            for (int j = 0; j < children.length; j++) {
                                IModelElement child = children[j];
                                if (child.getElementName().equals(variableName) && child instanceof SourceField) {
                                    selection.add(child);
                                    return;
                                }
                            }
                        } catch (ModelException e) {
                        }
                    }
                }
            }
        }// method scope

        selectOnVarInType(sourceClass, variableName);
    }

    private void selectOnVarInType(IType classType, String varName) {
        try {
            IField[] fields = classType.getFields();
            for (int i = 0; i < fields.length; i++) {
                if (fields[i] != null && fields[i].getElementName().equalsIgnoreCase(varName)) {
                    selection.add(fields[i]);
                    return;
                }
            }
            String[] superClasses = classType.getSuperClasses();
            if (superClasses != null && superClasses.length > 0) {
                for (int i = 0; i < superClasses.length; i++) {
                    SourceType superClass = mixinModel.findClass(superClasses[i]);
                    if (superClass != null) {
                        selectOnVarInType(superClass, varName);
                    }
                }
            }
        } catch (ModelException e) {
        }
    }

    private List<IMethod> selectOnMethdCall(USMethodCallExpression methodCall) {
        return selectOnMethodInType(sourceClass, methodCall.getName());
    }

    private List<IMethod> selectOnMethodInType(IType classType, String methodName) {
        List<IMethod> resultMethods = new ArrayList<IMethod>();
        try {
            IMethod[] methods = classType.getMethods();
            for (int i = 0; i < methods.length; i++) {
                if (methods[i] != null && methods[i].getElementName().equalsIgnoreCase(methodName)) {
                    resultMethods.add(methods[i]);
                }
            }
            String[] superClasses = classType.getSuperClasses();
            if (superClasses != null && superClasses.length > 0) {
                for (int i = 0; i < superClasses.length; i++) {
                    SourceType superClass = mixinModel.findClass(superClasses[i]);
                    if (superClass != null) {
                        resultMethods.addAll(selectOnMethodInType(superClass, methodName));
                    }
                }
            }
        } catch (ModelException e) {
        }
        return resultMethods;
    }

    private void selectOnTypeReference(USTypeReference usTypeRef) {
        USTypeReference classType = usTypeRef;
        TypeNameMatchRequestor typeNameRequestor = new TypeNameMatchRequestor() {
            public void acceptTypeNameMatch(TypeNameMatch match) {
                selection.add(match.getType());
            }
        };
        USModelHelper.searchTypeDeclarations(sourceModule.getScriptProject(), classType.getName(), typeNameRequestor);
    }

    public static ASTNode findMinimalNode(ModuleDeclaration unit, int start, int end) {

        class Visitor extends ASTVisitor {
            ASTNode result = null;
            final int start, end;

            public Visitor(int start, int end) {
                this.start = start;
                this.end = end;
            }

            public ASTNode getResult() {
                return result;
            }

            public boolean visitGeneral(ASTNode node) throws Exception {
                int realStart = node.sourceStart();
                int realEnd = node.sourceEnd();

                if (realStart <= start && realEnd >= end) {
                    if (result != null) {
                        if ((node.sourceStart() >= result.sourceStart()) && (node.sourceEnd() <= result.sourceEnd()))
                            result = node;
                    } else {
                        result = node;
                    }
                }
                return true;
            }

        }

        Visitor visitor = new Visitor(start, end);

        try {
            unit.traverse(visitor);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return visitor.getResult();
    }

    public static Set<Declaration> collectVarDeclaration(USFuncDeclaration func, final int offset) {
        final Set<Declaration> setOfVars = new HashSet<Declaration>();

        ASTVisitor visitor = new ASTVisitor() {
            @Override
            public boolean visitGeneral(ASTNode node) throws Exception {
                if (node.sourceEnd() < offset && (node instanceof USVariableDeclaration || node instanceof USArgument)) {
                    setOfVars.add((Declaration) node);
                }
                return super.visitGeneral(node);
            }

            @Override
            public void endvisitGeneral(ASTNode node) throws Exception {
                super.endvisitGeneral(node);
            }
        };

        try {
            func.traverse(visitor);
        } catch (Throwable e) {
            e.printStackTrace();
        }

        return setOfVars;
    }

    public static ASTNode[] collectWayToNode(ModuleDeclaration module, final ASTNode targetNode) {

        final Stack<ASTNode> stack = new Stack<ASTNode>();

        ASTVisitor visitor = new ASTVisitor() {
            boolean found = false;

            @Override
            public boolean visitGeneral(ASTNode node) throws Exception {
                if (!found) {
                    stack.push(node);
                    if (node.locationMatches(targetNode)) {
                        found = true;
                    }
                }
                return super.visitGeneral(node);
            }

            @Override
            public void endvisitGeneral(ASTNode node) throws Exception {
                super.endvisitGeneral(node);
                if (!found) {
                    stack.pop();
                }
            }
        };

        try {
            module.traverse(visitor);
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return stack.toArray(new ASTNode[stack.size()]);
    }
}
